先讓我們從 useState 認識起,他背後的原理其實是利用 JS 的陣列解構:
const number = [1, 2];
const [one, two] = number;
console.log(one, two); // 印出 1 和 2
useState 會回傳一個陣列,在這個陣列裡包含兩個東西: State 和可以改變 State 的函式。當我們宣告陣列,對應到 useState 時,就賦予我們自己定義的變數新的意義。不只方便開發者使用,也能同時完成變數宣告和套用 React 設好的 useState 兩件事。
const [text, setText] = useState('input value');
// text 對應到 State
// setText 對應到 setState()
前面聽起來很簡單對吧?但一開始學習 React 時, State 更新機制絕對是我碰到數一數二的大魔王。這必須「歸功」於當時的我 JS 基礎不夠穩,只覺得東西有跑出來就好,沒有仔細研究這個語法和那個語法差在哪。
在 React 的世界中,要判斷一個值是否更新,並不是用直接在舊值上更新的方式,而是產生一個新值後直接取代。這也是為什麼前面一再強調,更新值必須透過 setState() 來更新,而不是其他方式。
// 正確
setText('update input value');
// 錯誤
text = 'update input value';
this.text = 'update input value';
我們可以實驗看看,把程式碼改成這樣:
function App() {
let text = 'input value';
return (
<SafeAreaView>
<View>
<Text>Text: {text}</Text>
<TextInput
value={text}
onChangeText={newText => {
text = newText;
console.log(text);
}}
/>
會發現,雖然 console.log()
會印出更新後的值,但是畫面並沒有被正確更新。其實這是 React 用來省效能的一種方式。因為如果要一一比對每個值是否改變,再去更新那些有改變的值,當值十分複雜時,要花很多效能處理。因此 React 索性不比對了,只要直接去更改原始記憶體空間的,一律視為沒更新。只有產生新值直接取代,會被判定需要更新畫面。
你可能會覺得這跟我有何關係,何必大費周章講這麼多,反正要乖乖透過 useState 初始化,並用裡頭設定的 setState 更新值就是了嘛!讓我們接著來看下面的例子:
function App() {
const [textArr, setTextArr] = useState(['a', 'b', 'c']);
return (
<SafeAreaView>
<View>
{textArr.map(text => (
<Text key={text}>Text: {text}</Text>
))}
<TouchableOpacity
onPress={() => {
const newTextArr = textArr.push('d');
setTextArr(newTextArr);
console.log(textArr);
}}>
<Text>按按鈕會新增 d</Text>
</TouchableOpacity>
我們把資料改為字串陣列,設定一個按鈕,當用戶按下後,會將 d 這個字 push 進陣列中,用 setState 更新,並重新渲染畫面讓畫面上從「 Text: a Text: b Text:c 」變為「 Text: a Text: b Text:c Text:d 」。
不過當我們按下按鈕會發現,再一次的, console.log()
有顯示更新值,畫面卻壞掉了。為什麼呢?
因為 push 這個方法,會直接更動原陣列。我們應該使用 map 、 filter 等會產生新值的方式去更新,否則即使透過 setState 做最後的更新,一樣會因改到原始記憶體空間,讓 React 誤以為值沒被更改。
React 18 後的 Functional Component 還有一個特點,是當 State 更新,會整個 function 從頭跑過一次。並不是只去更改一兩個變動的部分,而是採取一率將對應區塊全數清除,用更新後的完整資料全部重繪。
當然,如果該 Functional Component 裡有不只一個 State ,他會等到整個 function 都跑完了,確定有哪些 State 更新了,才一次重跑,不然太耗效能。
不過這種一率全部清除,再重新渲染的方式,怎麼想都很容易導致畫面卡住。因此, React 並不是直接操作真實 DOM 元素,而是透過 Reconcilier 在 Virtual DOM 上產生、管理 React Elements ,再比較新舊 Virtual DOM ,將差異給 Renderer 更新真實 DOM 。這樣的好處除了節省效能,也因為處理 React Elements 和處理渲染畫面的部分分開,使 React 在操作 DOM 上保有彈性,搭配 React DOM 就能處理瀏覽器、搭配 React Native 就能處理App 。